/*
 *   Licensed to the Apache Software Foundation (ASF) under one
 *   or more contributor license agreements.  See the NOTICE file
 *   distributed with this work for additional information
 *   regarding copyright ownership.  The ASF licenses this file
 *   to you under the Apache License, Version 2.0 (the
 *   "License"); you may not use this file except in compliance
 *   with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing,
 *   software distributed under the License is distributed on an
 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *   KIND, either express or implied.  See the License for the
 *   specific language governing permissions and limitations
 *   under the License.
 *
 */

package org.apache.directory.server.http;


import static org.apache.directory.server.http.AppConstants.*;
import static org.apache.directory.server.http.AppConstants.BIND_URI;
import static org.apache.directory.server.http.AppConstants.LDAP_RESULT_KEY;
import static org.apache.directory.server.http.AppConstants.MODIFY_URI;
import static org.apache.directory.server.http.AppConstants.SEARCH_RESULTS_KEY;
import static org.apache.directory.server.http.AppConstants.SEARCH_URI;
import static org.apache.directory.server.http.AppConstants.SESSION_ID;
import static org.apache.directory.server.http.AppConstants.UNBIND_URI;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.server.BindResponseHolder;
import org.apache.directory.server.codec.json.JsonBuilder;
import org.apache.directory.server.core.EntryToResponseCursor;
import org.apache.directory.shared.ldap.entry.Entry;
import org.apache.directory.shared.ldap.exception.LdapException;
import org.apache.directory.shared.ldap.message.AbstractResultResponse;
import org.apache.directory.shared.ldap.message.AddRequestImpl;
import org.apache.directory.shared.ldap.message.AddResponse;
import org.apache.directory.shared.ldap.message.BindRequestImpl;
import org.apache.directory.shared.ldap.message.BindResponse;
import org.apache.directory.shared.ldap.message.CompareRequestImpl;
import org.apache.directory.shared.ldap.message.CompareResponse;
import org.apache.directory.shared.ldap.message.DeleteRequestImpl;
import org.apache.directory.shared.ldap.message.DeleteResponse;
import org.apache.directory.shared.ldap.message.ModifyDnRequestImpl;
import org.apache.directory.shared.ldap.message.ModifyDnResponse;
import org.apache.directory.shared.ldap.message.ModifyRequest;
import org.apache.directory.shared.ldap.message.ModifyRequestImpl;
import org.apache.directory.shared.ldap.message.ModifyResponse;
import org.apache.directory.shared.ldap.message.Response;
import org.apache.directory.shared.ldap.message.ResultCodeEnum;
import org.apache.directory.shared.ldap.message.ResultResponse;
import org.apache.directory.shared.ldap.message.SearchRequestImpl;
import org.apache.directory.shared.ldap.message.SearchResultEntry;
import org.apache.directory.shared.ldap.util.StringTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;


/**
 * TODO AuthenticationServlet.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class HttpDirectoryServlet extends HttpServlet
{

    private static final Logger LOG = LoggerFactory.getLogger( HttpDirectoryServlet.class );

    private int sessionInterval = 20 * 60 * 60;

    private static final ConcurrentHashMap<String, LdapConnection> CONNECTIONS = new ConcurrentHashMap<String, LdapConnection>();

    private Type commonRespType = new TypeToken<AbstractResultResponse>()
    {
    }.getType();


    @Override
    protected void doPost( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException
    {

        String uri = req.getRequestURI();

        LOG.debug( "received request to {} from {}", uri, req.getRemoteAddr() );

        uri = removeTrailingSlash( uri );

        String sessionId = req.getHeader( SESSION_ID );

        if ( sessionId != null )
        {
            if ( !CONNECTIONS.containsKey( sessionId ) )
            {
                LOG.debug( "no session exists with the received session id {}", sessionId );
                sessionId = null;
            }
        }

        if ( uri.endsWith( BIND_URI ) )
        {
            if ( sessionId != null )
            {
                resp.getWriter().write( "{'message':'session already exists'}" );
                return;
            }

            bind( req, resp );
            return;
        }

        if ( sessionId == null )
        {
            resp.getWriter().write( "{'message':'there is no session associated, need to rebind'}" );
            return;
        }

        if ( uri.endsWith( UNBIND_URI ) )
        {
            unbind( req, resp, sessionId );
            return;
        }

        LdapConnection connection = CONNECTIONS.get( sessionId );

        if ( uri.endsWith( SEARCH_URI ) )
        {
            search( req, resp, connection );
        }
        else if ( uri.endsWith( MODIFY_URI ) )
        {
            modify( req, resp, connection );
        }
        else if ( uri.endsWith( ADD_URI ) )
        {
            add( req, resp, connection );
        }
        else if ( uri.endsWith( DELETE_URI ) )
        {
            delete( req, resp, connection );
        }
        else if ( uri.endsWith( MODIFY_DN_URI ) )
        {
            modifyDn( req, resp, connection );
        }
        else if ( uri.endsWith( COMPARE_URI ) )
        {
            compare( req, resp, connection );
        }
    }


    private void bind( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, IOException
    {
        String json = getPostData( req );

        BindRequestImpl bindReq = JsonBuilder.get().fromJson( json, BindRequestImpl.class );
        BindResponseHolder bindRespHolder = HttpDirectoryServiceHolder.get().bind( bindReq );

        BindResponse bindResp = bindRespHolder.getResponse();

        if ( bindResp.getLdapResult().getResultCode() == ResultCodeEnum.SUCCESS )
        {
            LdapConnection connection = bindRespHolder.getConnection();

            String key = UUID.randomUUID().toString();

            CONNECTIONS.put( key, connection );

            resp.setHeader( SESSION_ID, key );
        }

        resp.getWriter().write( JsonBuilder.get().toJson( bindResp ) );
        resp.getWriter().flush();
    }


    private void search( HttpServletRequest req, HttpServletResponse resp, LdapConnection connection )
        throws ServletException, IOException
    {
        String reqJson = getPostData( req );

        SearchRequestImpl searchReq = JsonBuilder.get().fromJson( reqJson, SearchRequestImpl.class );
        try
        {
            EntryToResponseCursor<Response> cursor = ( EntryToResponseCursor<Response> ) connection.search( searchReq );
            cursor.beforeFirst();

            JsonObject jsonResp = new JsonObject();

            JsonArray ja = new JsonArray();
            Gson builder = JsonBuilder.get();

            while ( cursor.next() )
            {
                Response searchResp = cursor.get();
                if ( searchResp instanceof SearchResultEntry )
                {
                    Entry entry = ( ( SearchResultEntry ) searchResp ).getEntry();

                    LOG.debug( "processing search result entry with dn {}", entry.getDn() );

                    JsonElement entryElement = builder.toJsonTree( entry );
                    ja.add( entryElement );
                }
            }

            cursor.close();
            
            jsonResp.add( SEARCH_RESULTS_KEY, ja );

            JsonElement ldapResult = builder.toJsonTree( cursor.getSearchDone() );
            jsonResp.add( LDAP_RESULT_KEY, ldapResult.getAsJsonObject().get( LDAP_RESULT_KEY ) );

            PrintWriter pw = resp.getWriter();
            pw.write( jsonResp.toString() );
            pw.flush();
        }
        catch ( Exception e )
        {
            e.printStackTrace();
        }
    }


    private void modify( HttpServletRequest req, HttpServletResponse resp, LdapConnection connection )
        throws ServletException, IOException
    {
        String jsonReq = getPostData( req );
        ModifyRequest modReq = JsonBuilder.fromJson( jsonReq, ModifyRequestImpl.class );

        try
        {
            ModifyResponse modResp = connection.modify( modReq );
            writeResultResponseJson( modResp, resp );
        }
        catch ( LdapException e )
        {
            e.printStackTrace();
        }
    }


    private void unbind( HttpServletRequest req, HttpServletResponse resp, String sessionId ) throws ServletException,
        IOException
    {
        try
        {
            LdapConnection connection = CONNECTIONS.remove( sessionId );
            connection.unBind();
            connection = null;
            LOG.debug( "connection with session id {} was successfully unbound", sessionId );
        }
        catch ( Exception e )
        {
            e.printStackTrace();
        }
    }


    private void add( HttpServletRequest req, HttpServletResponse resp, LdapConnection connection )
        throws ServletException, IOException
    {

        try
        {
            String jsonReq = getPostData( req );
            AddRequestImpl addReq = JsonBuilder.fromJson( jsonReq, AddRequestImpl.class );
            AddResponse addResp = connection.add( addReq );
            writeResultResponseJson( addResp, resp );
        }
        catch ( LdapException e )
        {
            e.printStackTrace();
        }
    }


    private void delete( HttpServletRequest req, HttpServletResponse resp, LdapConnection connection )
        throws ServletException, IOException
    {

        try
        {
            String jsonReq = getPostData( req );
            DeleteRequestImpl deleteReq = JsonBuilder.fromJson( jsonReq, DeleteRequestImpl.class );
            DeleteResponse deleteResp = connection.delete( deleteReq );
            writeResultResponseJson( deleteResp, resp );
        }
        catch ( LdapException e )
        {
            e.printStackTrace();
        }
    }


    private void modifyDn( HttpServletRequest req, HttpServletResponse resp, LdapConnection connection )
        throws ServletException, IOException
    {

        try
        {
            String jsonReq = getPostData( req );
            ModifyDnRequestImpl modDnReq = JsonBuilder.fromJson( jsonReq, ModifyDnRequestImpl.class );
            ModifyDnResponse modDnResp = connection.modifyDn( modDnReq );
            writeResultResponseJson( modDnResp, resp );
        }
        catch ( LdapException e )
        {
            e.printStackTrace();
        }
    }


    private void compare( HttpServletRequest req, HttpServletResponse resp, LdapConnection connection )
        throws ServletException, IOException
    {

        try
        {
            String jsonReq = getPostData( req );
            CompareRequestImpl compareReq = JsonBuilder.fromJson( jsonReq, CompareRequestImpl.class );
            CompareResponse compareResp = connection.compare( compareReq );
            writeResultResponseJson( compareResp, resp );
        }
        catch ( LdapException e )
        {
            e.printStackTrace();
        }
    }


    private void writeResultResponseJson( ResultResponse resultResponse, HttpServletResponse resp ) throws IOException
    {
        String jsonResp = JsonBuilder.toJson( resultResponse, commonRespType );

        PrintWriter pw = resp.getWriter();
        pw.write( jsonResp.toString() );
        pw.flush();
    }


    private String getPostData( HttpServletRequest req )
    {
        try
        {
            int len = req.getContentLength();
            InputStream in = req.getInputStream();

            ByteBuffer data = ByteBuffer.allocate( len );

            byte[] buf = new byte[1024];

            while ( true )
            {
                int read = in.read( buf );
                if ( read > 0 )
                {
                    data.put( buf, 0, read );
                }
                else
                {
                    break;
                }
            }

            data.flip();

            String received = StringTools.utf8ToString( data.array() );

            LOG.debug( "received data {}", received );

            return received;
        }
        catch ( IOException e )
        {
            e.printStackTrace();
        }

        return null;
    }


    private String removeTrailingSlash( String uri )
    {
        if ( uri.endsWith( "/" ) )
        {
            uri = uri.substring( 0, uri.length() - 1 );
            return removeTrailingSlash( uri );
        }

        return uri;
    }
}
